/*
 * File     : StructureBuilder.java
 * Created  : 1 May 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.structure;

import static com.googlecode.dni.internal.DniInternal.LOGGER;
import static com.googlecode.dni.internal.DniTypes.*;
import static org.objectweb.asm.Opcodes.*;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.Buffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import com.googlecode.dni.ByValue;
import com.googlecode.dni.Padding;
import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.internal.MemoryAccess;
import com.googlecode.dni.type.LifetimeManagedObject;
import com.googlecode.dni.type.NativeObject;
import com.googlecode.dni.type.NativeObjectList;
import com.googlecode.dni.type.Pointer;
import com.googlecode.dni.type.structure.FieldIndex;
import com.googlecode.dni.type.structure.FieldOffset;
import com.googlecode.dni.type.structure.IllegalStructureException;
import com.googlecode.dni.type.structure.Structure;
import com.googlecode.dni.type.structure.StructureFactory;
import com.googlecode.dni.type.structure.StructureInfo;
import com.googlecode.dni.type.structure.StructureOptions;

/**
 * <p>
 *  Generates byte-code for implementations of structure types.
 * </p>
 *
 * @param <T>
 *            the structure interface type
 *
 * @author Matthew Wilson
 */
@SuppressWarnings( "nls" )
public final class StructureBuilder< T extends Structure >
{

    /** Whether we've warned about missing class files. */
    private static boolean warnedAboutMissingClassFile;

    static
    {
        // TODO assert PACKAGE.equals( ... some class's package ... );
    }


    private final Class< T > type;

    private int size;

    private Padding padding;

    private boolean opaque;

    /** The interface name (in byte-code style). */
    private final String interfaceName;

    /** The type implementation name (in byte-code style). */
    private final String implName;

    private final org.objectweb.asm.Type implType;

    private final String factoryImplName;

    private final boolean lifetimeManaged;

    private List< StructureField > fields = Collections.emptyList();

    /** The largest structure field unit size; used for padding calculations. */
    private int largestFieldUnitSize;


    /**
     * Creates a new instance.
     *
     * @param <T>
     *            the structure interface type
     * @param type
     *            the structure interface type
     * @param implName
     *            the implementation type name, in byte-code style
     * @param factoryImplName
     *            the factory implementation type name, in byte-code style
     * @return a new instance
     */
    public static < T extends Structure > StructureBuilder< T > create( final Class< T > type,
                                                                        final String implName,
                                                                        final String factoryImplName )
    {
        return new StructureBuilder< T >( type, implName, factoryImplName );
    }

    /**
     * @param type
     *            the type
     * @param implName
     *            the implementation type name, in byte-code style
     * @param factoryImplName
     *            the factory implementation type name, in byte-code style
     */
    private StructureBuilder( final Class< T > type, final String implName, final String factoryImplName )
    {
        if ( !type.isInterface() )
        {
            throw new IllegalStructureException( "Structure must an interface: " + type.getName() );
        }

        if ( type.getTypeParameters().length > 0 )
        {
            throw new IllegalStructureException( "Structure cannot have type parameters: " + type.getName() );
        }

        this.type = type;

        StructureOptions options = this.type.getAnnotation( StructureOptions.class );
        this.size = -1;
        this.padding = Padding.DEFAULT;
        this.opaque = false;
        if ( options != null )
        {
            this.size = options.size();
            this.padding = options.padding();
            this.opaque = options.opaque();
        }

        this.interfaceName = this.type.getName().replace( '.', '/' );
        this.implName = implName;
        this.implType = org.objectweb.asm.Type.getType( "L" + this.implName + ";" );
        this.factoryImplName = factoryImplName;
        this.lifetimeManaged = LifetimeManagedObject.class.isAssignableFrom( type );
    }

    /**
     * Creates and loads the class.
     *
     * @return the class
     */
    public Class< ? extends T > createClass()
    {
        LOGGER.info( "Creating Structure implementation for: " + this.type.getName() );

        // 1) determine what (relevant) methods there are
        List< Method > methods = getFieldMethods();

        byte[] classFile;

        if ( this.opaque )
        {
            classFile = createOpaque();
        }
        else
        {
            // 2) find getters, defining fields
            Map< String, StructureField > fieldMap = new HashMap< String, StructureField >();
            findFieldGetters( methods, fieldMap );

            // 3) find equivalent setters
            findFieldSetters( methods, fieldMap );

            if ( !methods.isEmpty() )
            {
                throw new IllegalStructureException( "Unrecognised extra method on structure: " + methods.get( 0 ) );
            }

            // 4) check all fields are complete
            checkSetters( fieldMap );

            // 5) sort the fields by index
            sortFields( fieldMap.values() );

            // 6) allocate offsets
            int calculatedSize = setOffsets( this.padding );
            if ( this.size < 0 )
            {
                this.size = calculatedSize;
            }
            else if ( this.size < calculatedSize )
            {
                LOGGER.warning( "Fixed size is too small for " + this.type.getName()
                                + "; calculcated = " + calculatedSize + ", fixed = " + this.size );
            }

            // 7) create the class
            classFile = writeClass();
        }

        Class< ? > implClass = DniInternal.defineClass( this.implName,
                                                        this.type.getClassLoader(),
                                                        classFile );

        return implClass.asSubclass( this.type );
    }

    /**
     * Creates the factory.
     *
     * @return the factory
     */
    public StructureFactory< T > createFactory()
    {
        ClassWriter classWriter = new ClassWriter( ClassWriter.COMPUTE_FRAMES );

        String baseType;
        if ( this.lifetimeManaged || this.opaque )
        {
            // neither lifetime-managed nor opaque structures need any kind of
            // management of created instances
            baseType = ABSTRACT_LIFETIME_MANAGED_STRUCTURE_FACTORY;
        }
        else
        {
            baseType = ABSTRACT_AUTO_FREED_STRUCTURE_FACTORY;
        }

        classWriter.visit( Opcodes.V1_6,
                           Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL,
                           this.factoryImplName,
                           baseType + "<L" + this.interfaceName + ";>",
                           baseType,
                           null );

        classWriter.visitField( ACC_PUBLIC | ACC_STATIC | ACC_FINAL,
                                "INSTANCE",
                                "L" + this.factoryImplName + ";",
                                null,
                                null );

        createClassInitialiser( classWriter );
        createFactoryConstructor( classWriter, baseType );
        writeUntypedCreateMethod( classWriter );
        writeUntypedFromPointerMethod( classWriter );

        byte[] classFile = classWriter.toByteArray();

        Class< ? > clazz = DniInternal.defineClass( this.factoryImplName,
                                                    this.type.getClassLoader(),
                                                    classFile );
        @SuppressWarnings( "unchecked" )
        Class< ? extends StructureFactory< T > > factoryClass =
            (Class< ? extends StructureFactory< T > >) clazz;

        try
        {
            @SuppressWarnings( "unchecked" )
            StructureFactory< T > structureFactory =
                (StructureFactory< T >) factoryClass.getField( "INSTANCE" ).get( null );
            return structureFactory;
        }
        catch ( Exception exception )
        {
            throw new IllegalStructureException( "Bad factory", exception );
        }
    }

    /**
     * Performs post-create actions.
     */
    public void postCreate()
    {
        for ( StructureField field : this.fields )
        {
            field.postCreate();
        }
    }

    /**
     * Performs all the build operations.
     *
     * @return the structure record
     */
    public StructureRecord build()
    {
        // TODO make the createClass() & createFactory() methods private-ish

        Class< ? extends T > impl = createClass();
        StructureFactory< T > factory = createFactory();
        StructureInfo info = new StructureInfo( this.size );

        return new StructureRecord( this.type,
                                    impl,
                                    factory,
                                    info,
                                    this.largestFieldUnitSize );
    }

    /**
     * Writes a constant instruction using the most optimal byte-code form.
     *
     * @param methodVisitor
     *            the method visitor
     * @param value
     *            the value
     */
    // TODO move me somewhere
    public static void writeConstantInt( final MethodVisitor methodVisitor, final int value )
    {
        if ( value >= -1 && value <= 5 )
        {
            methodVisitor.visitInsn( ICONST_0 + value );
        }
        else if ( value >= -128 && value <= 127 )
        {
            methodVisitor.visitIntInsn( BIPUSH, value );
        }
        else if ( value >= -32768 && value <= 32767 )
        {
            methodVisitor.visitIntInsn( SIPUSH, value );
        }
        else
        {
            methodVisitor.visitLdcInsn( value );
        }
    }

    private List< Method > getFieldMethods()
    {
        List< Method > methods = new ArrayList< Method >();
        for ( Method method : this.type.getMethods() )
        {
            if ( method.getDeclaringClass() != Object.class
                 && method.getDeclaringClass() != Structure.class
                 && method.getDeclaringClass() != NativeObject.class
                 && method.getDeclaringClass() != LifetimeManagedObject.class )
            {
                methods.add( method );
            }
        }

        // There's no guarantee of ordering; Java 6 seems to sort them, but not
        // in all circumstances (yes, that's confusing).
        // Try to find the source class file and use that for the ordering.
        // (Of course, that relies on javac ordering them correctly :-S )

        methods = sortMethods( methods );

        if ( methods.isEmpty() && !this.opaque )
        {
            throw new IllegalArgumentException( "Structure must define at least one field: " + this.type.getName() );
        }
        if ( !methods.isEmpty() && this.opaque )
        {
            throw new IllegalArgumentException( "Opaque structure must define no fields: " + this.type.getName() );
        }

        return methods;
    }

    /**
     * Sorts the methods according to the original class file.  Yeah, thanks a
     * BUNCH for preserving the order.
     *
     * @param methods
     *            the unsorted methods
     * @return the sorted methods
     */
    private List< Method > sortMethods( final List< Method > methods )
    {
        String name = this.type.getName().replace( '.', '/' ) + ".class";
        InputStream stream = this.type.getClassLoader().getResourceAsStream( name );
        if ( stream == null )
        {
            warnAboutMissingClassFile();
        }
        else
        {
            MethodSortingVisitor methodSortingVisitor = new MethodSortingVisitor( methods );
            try
            {
                ClassReader reader = new ClassReader( stream );
                reader.accept( methodSortingVisitor,
                               ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES );
                return methodSortingVisitor.getSortedMethods();
            }
            catch ( IOException exception )
            {
                LOGGER.log( Level.WARNING, "Failed to read class file", exception );
                warnAboutMissingClassFile();
            }
            finally
            {
                try
                {
                    stream.close();
                }
                catch ( IOException exception )
                {
                    LOGGER.log( Level.WARNING, "Failed to close input stream", exception );
                }
            }
        }
        return methods;
    }

    /**
     * Warns about missing / broken class files.
     */
    private void warnAboutMissingClassFile()
    {
        if ( warnedAboutMissingClassFile == false )
        {
            LOGGER.warning( "Cannot file class file to determine structure field ordering" );
            LOGGER.warning( "You will receive this warning only once" );
            warnedAboutMissingClassFile = true;
        }
    }

    private void findFieldGetters( final List< Method > methods,
                                   final Map< String, StructureField > fieldMap )
    {
        for ( Iterator< Method > iterator = methods.iterator(); iterator.hasNext(); )
        {
            // TODO Tidy this method... it's too long

            Method method = iterator.next();
            boolean isGetter = method.getName().startsWith( "get" );
            boolean isUpdate = method.getName().startsWith( "update" );
            if ( isGetter || isUpdate )
            {
                Class< ? >[] parameterTypes = method.getParameterTypes();
                Class< ? > returnType = method.getReturnType();
                Type genericReturnType = method.getGenericReturnType();

                if ( isGetter && parameterTypes.length > 0 )
                {
                    throw new IllegalStructureException( "Structure getter must take no parameters: " + method );
                }
                else if ( isUpdate )
                {
                    if ( parameterTypes.length != 1 || !Buffer.class.isAssignableFrom( parameterTypes[ 0 ] ) )
                    {
                        throw new IllegalStructureException( "Structure updater must take a Buffer parameter: " + method );
                    }

                    if ( returnType != boolean.class )
                    {
                        throw new IllegalStructureException( "Structure updater must return a boolean: " + method );
                    }

                    if ( method.getAnnotation( ByValue.class ) != null )
                    {
                        throw new IllegalStructureException( "Structure updater cannot be @ByValue: " + method );
                    }
                }

                StructureField field = null;
                String name;

                if ( isGetter )
                {
                    name = method.getName().substring( 3 );

                    boolean byValue = method.getAnnotation( ByValue.class ) != null;

                    if ( returnType.isPrimitive() )
                    {
                        field = new PrimitiveStructureField();
                    }
                    else if ( returnType == Pointer.class )
                    {
                        field = new PointerStructureField();
                    }
                    else if ( returnType.isInterface() && Structure.class.isAssignableFrom( returnType ) )
                    {
                        if ( byValue )
                        {
                            field = new StructureByValueStructureField();
                        }
                        else
                        {
                            field = new StructureByReferenceStructureField();
                        }
                    }
                    else if ( ( returnType == List.class || returnType == NativeObjectList.class )
                              && genericReturnType instanceof ParameterizedType )
                    {
                        ParameterizedType parameterizedType = (ParameterizedType) genericReturnType;
                        Type elementType = parameterizedType.getActualTypeArguments()[ 0 ];
                        if ( elementType instanceof Class< ? > )
                        {
                            Class< ? > elementClass = (Class< ? >) elementType;
                            if ( elementClass.isInterface() && Structure.class.isAssignableFrom( elementClass ) )
                            {
                                if ( !byValue )
                                {
                                    throw new IllegalStructureException( "List<Structure> must be @ByValue: " + method );
                                }
                                field = new StructureArrayStructureField();
                            }
                        }
                    }
                    else if ( !Modifier.isAbstract( returnType.getModifiers() )
                              && NativeObject.class.isAssignableFrom( returnType ) )
                    {
                        field = new NativeObjectStructureField();
                    }

                    if ( field == null )
                    {
                        throw new IllegalArgumentException( "Structure field unsupported type: " + method );
                    }
                }
                else
                {
                    name = method.getName().substring( 6 );
                    field = new BufferStructureField();
                }

                field.setGetter( method );
                field.setName( name );

                FieldIndex fieldIndex = method.getAnnotation( FieldIndex.class );
                field.setIndex( fieldIndex != null ? fieldIndex.value() : fieldMap.size() );

                FieldOffset fieldOffset = method.getAnnotation( FieldOffset.class );
                if ( fieldOffset != null )
                {
                    field.setOffset( fieldOffset.value() );
                }

                Object duplicate = fieldMap.put( field.getName(), field );
                if ( duplicate != null )
                {
                    throw new IllegalArgumentException( "Duplicate field name on structure: " + method.getDeclaringClass().getName()
                                                        + ": " + field.getName() );
                }

                // seen this method
                iterator.remove();
            }
        }
    }

    private void findFieldSetters( final List< Method > methods,
                                   final Map< String, StructureField > fieldMap )
    {
        for ( Iterator< Method > iterator = methods.iterator(); iterator.hasNext(); )
        {
            Method method = iterator.next();
            if ( method.getName().startsWith( "set" ) )
            {
                String name = method.getName().substring( 3 );
                StructureField field = fieldMap.get( name );
                if ( field == null )
                {
                    continue;
                }

                if ( method.getReturnType() != void.class )
                {
                    throw new IllegalArgumentException( "Structure setter must return void: " + method );
                }

                field.setSetter( method );

                iterator.remove();
            }
        }
    }

    private void checkSetters( final Map< String, StructureField > fieldMap )
    {
        for ( StructureField field : fieldMap.values() )
        {
            if ( field.expectsSetter() && field.getSetter() == null )
            {
                throw new IllegalArgumentException( "Missing setter on structure: " + this.type.getName() + ": " + field.getName() );
            }
            if ( !field.expectsSetter() && field.getSetter() != null )
            {
                throw new IllegalArgumentException( "Unexpected setter on structure: " + this.type.getName() + ": " + field.getName() );
            }
        }
    }

    private void sortFields( final Collection< StructureField > unsortedFields )
    {
        this.fields = new ArrayList< StructureField >( unsortedFields );
        Collections.sort( this.fields, new Comparator< StructureField >()
        {
            @Override
            public int compare( final StructureField o1, final StructureField o2 )
            {
                if ( o1.getIndex() < o2.getIndex() )
                {
                    return -1;
                }
                else if ( o1.getIndex() > o2.getIndex() )
                {
                    return 1;
                }

                throw new IllegalArgumentException( "Duplicate index on fields: " + o1.getName() + " and " + o2.getName()
                                                    + ": " + StructureBuilder.this.type.getName() );
            }
        } );

        if ( LOGGER.isLoggable( Level.FINE ) )
        {
            for ( final StructureField field : this.fields )
            {
                LOGGER.fine( " * " + field );
            }
        }
    }

    private int setOffsets( final Padding padding )
    {
        int offset = 0;
        int calculatedSize = 0;
        this.largestFieldUnitSize = 0;
        for ( StructureField field : this.fields )
        {
            int desiredOffset = field.getOffset();
            int fieldSize = field.getSize();
            int fieldPaddingUnitSize = field.getPaddingUnitSize();
            if ( desiredOffset < 0 )
            {
                offset = padding.pad( offset, fieldPaddingUnitSize );
                field.setOffset( offset );
            }

            this.largestFieldUnitSize = Math.max( this.largestFieldUnitSize, fieldPaddingUnitSize );

            offset += fieldSize;
            calculatedSize = Math.max( offset, calculatedSize );
        }

        calculatedSize = padding.pad( calculatedSize, this.largestFieldUnitSize );

        return calculatedSize;
    }

    /**
     * Writes the class with fields and methods.
     *
     * @return class file
     */
    private byte[] writeClass()
    {
        ClassWriter classWriter = new ClassWriter( ClassWriter.COMPUTE_FRAMES );
        classWriter.visit( Opcodes.V1_6,
                           Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL,
                           this.implName,
                           null,
                           "java/lang/Object",
                           new String[] { this.interfaceName } );

        classWriter.visitField( ACC_PUBLIC | ACC_FINAL, "pointer", "J", null, null );

        if ( !this.lifetimeManaged )
        {
            classWriter.visitField( ACC_PRIVATE | ACC_FINAL, "autoFree", "Z", null, null );
        }

        for ( StructureField field : this.fields )
        {
            field.writeFields( classWriter, this.implName );
        }

        writeDefaultConstructor( classWriter );
        writeCopyConstructor( classWriter );
        writePointerConstructor( classWriter );

        for ( StructureField field : this.fields )
        {
            field.writeMethods( classWriter, this.implName );
        }

        // Object methods
        writeEqualsMethod( classWriter );
        writeHashCodeMethod( classWriter );
        writeToStringMethod( classWriter );

        // neither lifetime-managed nor opaque require finalization:
        // in both cases, we didn't allocate the memory, so we mustn't free it
        if ( !this.lifetimeManaged && !this.opaque )
        {
            writeFinalizeMethod( classWriter );
        }

        // NativeObject methods
        writePointerMethod( classWriter );

        // Structure methods
        writeToDebugStringMethod( classWriter );
        writeCopyMethod( classWriter );
        writeCopyFromMethod( classWriter );

        if ( this.lifetimeManaged )
        {
            // LifetimeManagedObject methods
            throw new AssertionError( "I haven't implemented this yet" );
        }

        classWriter.visitEnd();

        return classWriter.toByteArray();
    }

    /**
     * Writes a default constructor.
     *
     * @param classWriter
     *            the class writer
     */
    private void writeDefaultConstructor( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC, "<init>", "()V", null, null );
        methodVisitor.visitCode();

        // super();
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitMethodInsn( INVOKESPECIAL, "java/lang/Object", "<init>", "()V" );

        // this.pointer = MemoryAccess.ALLOCATOR.allocate( <size> ).getAddress();
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitFieldInsn( GETSTATIC,
                                      MEMORY_ACCESS_TYPE,
                                      "ALLOCATOR",
                                      "L" + ALLOCATOR_TYPE + ";" );
        writeConstantInt( methodVisitor, this.size );
        methodVisitor.visitMethodInsn( INVOKEINTERFACE,
                                       ALLOCATOR_TYPE,
                                       "allocate",
                                       "(I)L" + POINTER_TYPE + ";" );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       POINTER_TYPE,
                                       "getAddress",
                                       "()J" );
        methodVisitor.visitFieldInsn( PUTFIELD, this.implName, "pointer", "J" );

        // we own the memory; auto-free it
        writeSetAutoFree( methodVisitor, true );

        for ( StructureField field : this.fields )
        {
            field.writeConstructorFragment( methodVisitor, this.implName );
        }

        // return
        methodVisitor.visitInsn( RETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes a copy constructor.
     *
     * @param classWriter
     *            the class writer
     */
    private void writeCopyConstructor( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               "<init>",
                                                               "(L" + this.implName + ";)V",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        // super();
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitMethodInsn( INVOKESPECIAL, "java/lang/Object", "<init>", "()V" );

        // this.pointer = MemoryAccess.ALLOCATOR.copy( other.pointer(), <size> ).getAddress();
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitFieldInsn( GETSTATIC,
                                      MEMORY_ACCESS_TYPE,
                                      "ALLOCATOR",
                                      "L" + ALLOCATOR_TYPE + ";" );

        methodVisitor.visitVarInsn( ALOAD, 1 );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       this.implName,
                                       "pointer",
                                       "()L" + POINTER_TYPE + ";" );

        writeConstantInt( methodVisitor, this.size );
        methodVisitor.visitMethodInsn( INVOKEINTERFACE,
                                       ALLOCATOR_TYPE,
                                       "copy",
                                       "(L" + POINTER_TYPE + ";I)L" + POINTER_TYPE + ";" );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       POINTER_TYPE,
                                       "getAddress",
                                       "()J" );
        methodVisitor.visitFieldInsn( PUTFIELD, this.implName, "pointer", "J" );

        // we own the memory; auto-free it
        writeSetAutoFree( methodVisitor, true );

        for ( StructureField field : this.fields )
        {
            field.writeConstructorFragment( methodVisitor, this.implName );
        }

        // return
        methodVisitor.visitInsn( RETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes a pointer constructor.
     *
     * @param classWriter
     *            the class writer
     */
    private void writePointerConstructor( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC, "<init>", "(J)V", null, null );
        methodVisitor.visitCode();

        // super();
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitMethodInsn( INVOKESPECIAL, "java/lang/Object", "<init>", "()V" );

        // this.pointer = pointer;
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitVarInsn( LLOAD, 1 );
        methodVisitor.visitFieldInsn( PUTFIELD, this.implName, "pointer", "J" );

        // we don't own the memory; don't auto-free it
        writeSetAutoFree( methodVisitor, false );

        for ( StructureField field : this.fields )
        {
            field.writeConstructorFragment( methodVisitor, this.implName );
        }

        // return
        methodVisitor.visitInsn( RETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes code to set the auto-free flag in the constructor.
     *
     * @param methodVisitor
     *            the method visitor
     * @param enable
     *            <code>true</code> to allow it to be created (when required);
     *            <code>false</code> to disable
     */
    private void writeSetAutoFree( final MethodVisitor methodVisitor,
                                   final boolean enable )
    {
        if ( enable && !this.lifetimeManaged )
        {
                methodVisitor.visitVarInsn( ALOAD, 0 );
                writeConstantInt( methodVisitor, 1 );
                methodVisitor.visitFieldInsn( PUTFIELD, this.implName, "autoFree", "Z" );
        }
        else
        {
            methodVisitor.visitVarInsn( ALOAD, 0 );
            writeConstantInt( methodVisitor, 0 );
            methodVisitor.visitFieldInsn( PUTFIELD, this.implName, "autoFree", "Z" );
        }
    }

    /**
     * Writes the {@link Structure#equals(Object)} method
     *
     * @param classWriter
     *            the class writer
     */
    private void writeEqualsMethod( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               "equals",
                                                               "(Ljava/lang/Object;)Z",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        // if ( obj == null )
        //     return false;
        methodVisitor.visitVarInsn( ALOAD, 1 );
        Label continueLabel1 = new Label();
        methodVisitor.visitJumpInsn( IFNONNULL, continueLabel1 );
        writeConstantInt( methodVisitor, 0 );
        methodVisitor.visitInsn( IRETURN );
        methodVisitor.visitLabel( continueLabel1 );

        // if ( this == obj )
        //     return true;
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitVarInsn( ALOAD, 1 );
        Label continueLabel2 = new Label();
        methodVisitor.visitJumpInsn( IF_ACMPNE, continueLabel2 );
        writeConstantInt( methodVisitor, 1 );
        methodVisitor.visitInsn( IRETURN );
        methodVisitor.visitLabel( continueLabel2 );

        // if ( obj.getClass() != getClass() )
        //     return false;
        methodVisitor.visitVarInsn( ALOAD, 1 );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;" );
        methodVisitor.visitLdcInsn( this.implType );
        Label continueLabel3 = new Label();
        methodVisitor.visitJumpInsn( IF_ACMPEQ, continueLabel3 );
        writeConstantInt( methodVisitor, 0 );
        methodVisitor.visitInsn( IRETURN );
        methodVisitor.visitLabel( continueLabel3 );

        // <Type> other = (<Type>) obj;
        methodVisitor.visitVarInsn( ALOAD, 1 );
        methodVisitor.visitTypeInsn( CHECKCAST, this.implName );
        methodVisitor.visitVarInsn( ASTORE, 2 );

        Label returnFalseLabel = new Label();

        for ( StructureField field : this.fields )
        {
            // if ( other.get<field>() != get<field>() )
            //     return false;
            field.writeEqualsFragment( methodVisitor, returnFalseLabel, this.implName );
        }

        // return true;
        writeConstantInt( methodVisitor, 1 );
        methodVisitor.visitInsn( IRETURN );

        // return false;
        methodVisitor.visitLabel( returnFalseLabel );
        writeConstantInt( methodVisitor, 0 );
        methodVisitor.visitInsn( IRETURN );

        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes the {@link Structure#hashCode()} method
     *
     * @param classWriter
     *            the class writer
     */
    private void writeHashCodeMethod( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               "hashCode",
                                                               "()I",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        // int hash = 0;
        methodVisitor.visitInsn( ICONST_0 );
        methodVisitor.visitVarInsn( ISTORE, 1 );

        for ( StructureField field : this.fields )
        {
            // hash = hash * 31 + <hash-of-value>;
            methodVisitor.visitVarInsn( ILOAD, 1 );
            writeConstantInt( methodVisitor, 31 );
            methodVisitor.visitInsn( IMUL );

            field.writeHashCodeFragment( methodVisitor, this.implName );

            methodVisitor.visitInsn( IXOR );
            methodVisitor.visitVarInsn( ISTORE, 1 );
        }

        // return hash;
        methodVisitor.visitVarInsn( ILOAD, 1 );
        methodVisitor.visitInsn( IRETURN );

        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes the {@link Structure#toString()} method
     *
     * @param classWriter
     *            the class writer
     */
    private void writeToStringMethod( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               "toString",
                                                               "()Ljava/lang/String;",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        // new StringBuilder()
        methodVisitor.visitTypeInsn( NEW, "java/lang/StringBuilder" );
        methodVisitor.visitInsn( DUP );
        methodVisitor.visitMethodInsn( INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V" );

        // .append( "<struct-name>[0x" )
        methodVisitor.visitLdcInsn( this.type.getName() + "[0x" );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "append",
                                       "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );

        // .append( Long.toHexString( this.pointer ) )
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitFieldInsn( GETFIELD, this.implName, "pointer", "J" );
        methodVisitor.visitMethodInsn( INVOKESTATIC,
                                       "java/lang/Long",
                                       "toHexString",
                                       "(J)Ljava/lang/String;" );

        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "append",
                                       "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );

        // .append( "]" )
        methodVisitor.visitLdcInsn( "]" );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "append",
                                       "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );

        // .toString()
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "toString",
                                       "()Ljava/lang/String;" );

        // return
        methodVisitor.visitInsn( ARETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes the {@link Object#finalize()} method
     *
     * @param classWriter
     *            the class writer
     */
    private void writeFinalizeMethod( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PROTECTED,
                                                               "finalize",
                                                               "()V",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        Label start = new Label();

        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitFieldInsn( GETFIELD,
                                      this.implName,
                                      "autoFree",
                                      "Z" );
        methodVisitor.visitJumpInsn( IFNE, start );
        methodVisitor.visitInsn( RETURN );

        Label end = new Label();
        Label handler = new Label();

        methodVisitor.visitTryCatchBlock( start, end, handler, "java/lang/Throwable" );

        methodVisitor.visitLabel( start );

        // <Factory>.INSTANCE.remove( this.pointer )
        methodVisitor.visitFieldInsn( GETSTATIC,
                                      this.factoryImplName,
                                      "INSTANCE",
                                      "L" + this.factoryImplName + ";" );
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitFieldInsn( GETFIELD,
                                      this.implName,
                                      "pointer",
                                      "J" );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       ABSTRACT_AUTO_FREED_STRUCTURE_FACTORY,
                                       "remove",
                                       "(J)V" );

        methodVisitor.visitFieldInsn( GETSTATIC,
                                      MEMORY_ACCESS_TYPE,
                                      "ALLOCATOR",
                                      "L" + ALLOCATOR_TYPE + ";" );
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       this.implName,
                                       "pointer",
                                       "()L" + POINTER_TYPE + ";" );

        methodVisitor.visitMethodInsn( INVOKEINTERFACE,
                                       ALLOCATOR_TYPE,
                                       "free",
                                       "(L" + POINTER_TYPE + ";)V" );
        methodVisitor.visitInsn( RETURN );
        methodVisitor.visitLabel( end );

        methodVisitor.visitLabel( handler );
        methodVisitor.visitVarInsn( ASTORE, 1 );

        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitVarInsn( ALOAD, 1 );
        methodVisitor.visitMethodInsn( INVOKESTATIC,
                                       STRUCTURE_HELPER_TYPE,
                                       "handleFinalizerThrowable",
                                       "(Ljava/lang/Object;Ljava/lang/Throwable;)V" );
        methodVisitor.visitInsn( RETURN );

        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes the {@link NativeObject#pointer()} method
     *
     * @param classWriter
     *            the class writer
     */
    private void writePointerMethod( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               "pointer",
                                                               "()L" + POINTER_TYPE + ";",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        methodVisitor.visitTypeInsn( NEW, POINTER_TYPE );
        methodVisitor.visitInsn( DUP );
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitFieldInsn( GETFIELD, this.implName, "pointer", "J" );
        methodVisitor.visitMethodInsn( INVOKESPECIAL,
                                       POINTER_TYPE,
                                       "<init>",
                                       "(J)V" );

        methodVisitor.visitInsn( ARETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes the {@link Structure#toString()} method
     *
     * @param classWriter
     *            the class writer
     */
    private void writeToDebugStringMethod( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               "toDebugString",
                                                               "()Ljava/lang/String;",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        // new StringBuilder()
        methodVisitor.visitTypeInsn( NEW, "java/lang/StringBuilder" );
        methodVisitor.visitInsn( DUP );
        methodVisitor.visitMethodInsn( INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V" );

        // .append( "<struct-name>[0x" )
        methodVisitor.visitLdcInsn( this.type.getName() + "[0x" );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "append",
                                       "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );

        // .append( Long.toHexString( this.pointer ) )
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitFieldInsn( GETFIELD, this.implName, "pointer", "J" );
        methodVisitor.visitMethodInsn( INVOKESTATIC,
                                       "java/lang/Long",
                                       "toHexString",
                                       "(J)Ljava/lang/String;" );

        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "append",
                                       "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );

        for ( StructureField field : this.fields )
        {
            // .append( ", <field-name>=" )
            methodVisitor.visitLdcInsn( ", " + field.getName() + "=" );
            methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                           "java/lang/StringBuilder",
                                           "append",
                                           "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );

            // .append( get<field>() )
            field.writeToStringFragment( methodVisitor, this.implName );
        }

        // .append( "]" )
        methodVisitor.visitLdcInsn( "]" );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "append",
                                       "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );

        // .toString()
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "toString",
                                       "()Ljava/lang/String;" );

        // return
        methodVisitor.visitInsn( ARETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes the {@link Structure#copy()} method
     *
     * @param classWriter
     *            the class writer
     */
    private void writeCopyMethod( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               "copy",
                                                               "()L" + STRUCTURE_TYPE + ";",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        // new <type>()
        methodVisitor.visitTypeInsn( NEW, this.implName );
        methodVisitor.visitInsn( DUP );
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitMethodInsn( INVOKESPECIAL, this.implName, "<init>", "(L" + this.implName + ";)V" );

        // return
        methodVisitor.visitInsn( ARETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes the {@link Structure#copyFrom(Structure)} method
     *
     * @param classWriter
     *            the class writer
     */
    private void writeCopyFromMethod( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               "copyFrom",
                                                               "(L" + STRUCTURE_TYPE + ";)V",
                                                               null,
                                                               null );
        methodVisitor.visitCode();

        methodVisitor.visitVarInsn( ALOAD, 1 );
        methodVisitor.visitTypeInsn( CHECKCAST, this.implName );
        methodVisitor.visitFieldInsn( GETFIELD, this.implName, "pointer", "J" );
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitFieldInsn( GETFIELD, this.implName, "pointer", "J" );
        writeConstantInt( methodVisitor, this.size );
        methodVisitor.visitInsn( I2L );
        methodVisitor.visitMethodInsn( INVOKESTATIC,
                                       MemoryAccess.UNSAFE_ACCESS_TYPE,
                                       "copyMemory",
                                       "(JJJ)V" );

        // return
        methodVisitor.visitInsn( RETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    private byte[] createOpaque()
    {
        String baseType;
        if ( this.lifetimeManaged )
        {
            baseType = ABSTRACT_OPAQUE_LIFETIME_MANAGED_STRUCTURE_TYPE;
        }
        else
        {
            baseType = ABSTRACT_OPAQUE_STRUCTURE_TYPE;
        }

        ClassWriter classWriter = new ClassWriter( ClassWriter.COMPUTE_FRAMES );
        classWriter.visit( Opcodes.V1_6,
                           Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL,
                           this.implName,
                           null,
                           baseType,
                           new String[] { this.interfaceName } );

        writeOpaquePointerConstructor( classWriter, baseType );

        classWriter.visitEnd();

        return classWriter.toByteArray();
    }

    /**
     * Writes a pointer constructor for the opaque case.
     *
     * @param classWriter
     *            the class writer
     * @param baseType
     *            the name of the base type
     */
    private void writeOpaquePointerConstructor( final ClassWriter classWriter,
                                                final String baseType )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC, "<init>", "(J)V", null, null );
        methodVisitor.visitCode();

        // super( pointer );
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitVarInsn( LLOAD, 1 );
        methodVisitor.visitMethodInsn( INVOKESPECIAL, baseType, "<init>", "(J)V" );

        // return
        methodVisitor.visitInsn( RETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes the static initialiser.
     *
     * @param classWriter
     *            the class writer
     */
    private void createClassInitialiser( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_STATIC,
                                                               "<clinit>",
                                                               "()V",
                                                               null,
                                                               null );
        methodVisitor.visitCode();
        methodVisitor.visitTypeInsn( NEW, this.factoryImplName );
        methodVisitor.visitInsn( DUP );
        methodVisitor.visitMethodInsn( INVOKESPECIAL,
                                       this.factoryImplName,
                                       "<init>",
                                       "()V" );
        methodVisitor.visitFieldInsn( PUTSTATIC,
                                      this.factoryImplName,
                                      "INSTANCE",
                                      "L" + this.factoryImplName + ";" );
        methodVisitor.visitInsn( RETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes a constructor for the factory class.
     *
     * @param classWriter
     *            the class writer
     * @param baseType
     *            the base type name
     */
    private void createFactoryConstructor( final ClassWriter classWriter,
                                           final String baseType )
    {
        MethodVisitor constructor = classWriter.visitMethod( Opcodes.ACC_PRIVATE,
                                                             "<init>",
                                                             "()V",
                                                             null,
                                                             null );
        constructor.visitCode();
        constructor.visitVarInsn( ALOAD, 0 );
        constructor.visitLdcInsn( org.objectweb.asm.Type.getType( this.type ) );
        constructor.visitMethodInsn( INVOKESPECIAL,
                                     baseType,
                                     "<init>",
                                     "(Ljava/lang/Class;)V" );
        constructor.visitInsn( RETURN );
        constructor.visitMaxs( 0, 0 );
        constructor.visitEnd();
    }

    /**
     * Writes the {@link AbstractStructureFactory#allocateUntyped()} method.
     *
     * @param classWriter
     *            the class writer
     */
    private void writeUntypedCreateMethod( final ClassWriter classWriter )
    {
        MethodVisitor create = classWriter.visitMethod( Opcodes.ACC_PUBLIC,
                                                        "allocateUntyped",
                                                        "()L" + STRUCTURE_TYPE + ";",
                                                        null,
                                                        null );
        create.visitCode();

        if ( this.opaque )
        {
            create.visitTypeInsn( NEW, "java/lang/UnsupportedOperationException" );
            create.visitInsn( DUP );
            create.visitLdcInsn( "Opaque structure: " + this.type.getName() );
            create.visitMethodInsn( INVOKESPECIAL,
                                    "java/lang/UnsupportedOperationException",
                                    "<init>",
                                    "(Ljava/lang/String;)V" );
            create.visitInsn( ATHROW );
        }
        else
        {
            create.visitTypeInsn( NEW, this.implName );
            create.visitInsn( DUP );
            create.visitMethodInsn( INVOKESPECIAL,
                                    this.implName,
                                    "<init>",
                                    "()V" );
            create.visitInsn( ARETURN );
        }

        create.visitMaxs( 0, 0 );
        create.visitEnd();
    }

    /**
     * Writes the {@link AbstractStructureFactory#fromPointerUntyped(long)}
     * method.
     *
     * @param classWriter
     *            the class writer
     */
    private void writeUntypedFromPointerMethod( final ClassWriter classWriter )
    {
        MethodVisitor create = classWriter.visitMethod( Opcodes.ACC_PUBLIC,
                                                        "fromPointerUntyped",
                                                        "(J)L" + STRUCTURE_TYPE + ";",
                                                        null,
                                                        null );
        create.visitCode();
        create.visitTypeInsn( NEW, this.implName );
        create.visitInsn( DUP );
        create.visitVarInsn( LLOAD, 1 );
        create.visitMethodInsn( INVOKESPECIAL,
                                this.implName,
                                "<init>",
                                "(J)V" );
        create.visitInsn( ARETURN );
        create.visitMaxs( 0, 0 );
        create.visitEnd();
    }

    private static final class MethodSortingVisitor implements ClassVisitor
    {

        private static final List< String > IGNORED_NAMES = Arrays.asList( "<init>", "<clinit>", "equals", "hashCode", "toString" );

        private final List< Method > methods = new ArrayList< Method >();

        private final List< Method > sortedMethods = new ArrayList< Method >();

        private boolean discrepancy;

        private String typedName;


        /**
         * @param methods
         *            the unsorted methods
         */
        MethodSortingVisitor( final List< Method > methods )
        {
            this.methods.addAll( methods );
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void visit( final int version,
                           final int access,
                           final String name,
                           final String signature,
                           final String superName,
                           final String[] interfaces )
        {
            this.typedName = name;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void visitSource( final String source, final String debug )
        {
            // don't care
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void visitOuterClass( final String owner, final String name, final String desc )
        {
            // don't care
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public AnnotationVisitor visitAnnotation( final String desc, final boolean visible )
        {
            // don't care
            return null;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void visitAttribute( final Attribute attr )
        {
            // don't care
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void visitInnerClass( final String name, final String outerName, final String innerName, final int access )
        {
            // don't care
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public FieldVisitor visitField( final int access,
                                        final String name,
                                        final String desc,
                                        final String signature,
                                        final Object value )
        {
            // don't care
            return null;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public MethodVisitor visitMethod( final int access,
                                          final String name,
                                          final String desc,
                                          final String signature,
                                          final String[] exceptions )
        {
            if ( IGNORED_NAMES.contains( name ) )
            {
                return null;
            }

            Method method = null;
            for ( Iterator< Method > iterator = this.methods.iterator(); iterator.hasNext(); )
            {
                Method candidate = iterator.next();
                if ( candidate.getName().equals( name ) )
                {
                    // no overloaded methods are supported by Structure, so it's
                    // safe just to check the name
                    method = candidate;
                    iterator.remove();
                    break;
                }
            }

            if ( method == null )
            {
                this.discrepancy = true;
            }
            else
            {
                this.sortedMethods.add( method );
            }

            return null;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void visitEnd()
        {
            // don't care
        }

        /**
         * @return the sorted methods
         */
        List< Method > getSortedMethods()
        {
            if ( this.discrepancy || !this.methods.isEmpty() )
            {
                LOGGER.warning( "Discrepancy between class file and loaded class: " + this.typedName );
            }

            return this.sortedMethods;
        }

    }
}
